package com.wdb.modules.security.config;

import com.wdb.annotation.AnonymousAccess;
import com.wdb.modules.security.config.bean.SecurityProperties;
import com.wdb.modules.security.security.JwtAccessDeniedHandler;
import com.wdb.modules.security.security.JwtAuthenticationEntryPoint;
import com.wdb.modules.security.security.TokenConfigurer;
import com.wdb.modules.security.security.TokenProvider;
import com.wdb.modules.security.service.OnlineUserService;
import com.wdb.modules.security.service.UserCacheManager;
import com.wdb.utils.enums.RequestMethodEnum;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.util.*;

/**
 * security 配置
 *
 * @author: wendaobai@qq.com
 * @date: 2024/1/10 0010 9:32
 */
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
  private final TokenProvider tokenProvider;
  private final SecurityProperties properties;
  private final OnlineUserService onlineUserService;
  private final UserCacheManager userCacheManager;
  private final CorsFilter corsFilter;

  private final JwtAuthenticationEntryPoint authenticationErrorHandler;
  private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
  private final ApplicationContext applicationContext;

  @Bean
  GrantedAuthorityDefaults grantedAuthorityDefaults() {
    // 去除 ROLE_ 前缀
    return new GrantedAuthorityDefaults("");
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    // 密码加密方式
    return new BCryptPasswordEncoder();
  }

  @Override
  protected void configure(HttpSecurity httpSecurity) throws Exception {
    RequestMappingHandlerMapping requestMappingHandlerMapping =
        (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");
    Map<RequestMappingInfo, HandlerMethod> handlerMethodMap =
        requestMappingHandlerMapping.getHandlerMethods();
    // 所有anonymous 注解的请求url
    Map<String, Set<String>> anonymousUrls = getAnonymousUrl(handlerMethodMap);

    System.out.println(anonymousUrls);
    // csrf 禁用
    httpSecurity
        .csrf()
        .disable()
        .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
        // 授权异常
        .exceptionHandling()
        .authenticationEntryPoint(authenticationErrorHandler)
        .accessDeniedHandler(jwtAccessDeniedHandler)
        // 防止iframe 造成跨域
        .and()
        .headers()
        .frameOptions()
        .disable()
        // 不创建会话
        .and()
        .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .authorizeRequests()
        // 静态资源等等
        .antMatchers(
            HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/webSocket/**")
        .permitAll()
        // swagger 文档
        .antMatchers("/swagger-ui.html")
        .permitAll()
        .antMatchers("/swagger-resources/**")
        .permitAll()
        .antMatchers("/webjars/**")
        .permitAll()
        .antMatchers("/*/api-docs")
        .permitAll()
        // 文件
        .antMatchers("/avatar/**")
        .permitAll()
        .antMatchers("/file/**")
        .permitAll()
        // 阿里巴巴 druid
        .antMatchers("/druid/**")
        .permitAll()
        // 放行OPTIONS请求
        .antMatchers(HttpMethod.OPTIONS, "/**")
        .permitAll()
        // 自定义匿名访问所有url放行：允许匿名和带Token访问，细腻化到每个 Request 类型
        // GET
        .antMatchers(
            HttpMethod.GET,
            anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0]))
        .permitAll()
        // POST
        .antMatchers(
            HttpMethod.POST,
            anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0]))
        .permitAll()
        // PUT
        .antMatchers(
            HttpMethod.PUT,
            anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0]))
        .permitAll()
        // PATCH
        .antMatchers(
            HttpMethod.PATCH,
            anonymousUrls.get(RequestMethodEnum.PATCH.getType()).toArray(new String[0]))
        .permitAll()
        // DELETE
        .antMatchers(
            HttpMethod.DELETE,
            anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0]))
        .permitAll()
        // 所有类型的接口都放行
        .antMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0]))
        .permitAll()
        // 所有请求都需要认证
        .anyRequest()
        .authenticated()
        .and()
        .apply(securityConfigurerAdapter());
  }

  private TokenConfigurer securityConfigurerAdapter() {
    return new TokenConfigurer(tokenProvider, properties, onlineUserService, userCacheManager);
  }

  private Map<String, Set<String>> getAnonymousUrl(
      Map<RequestMappingInfo, HandlerMethod> handlerMethodMap) {
    Map<String, Set<String>> anonymousUrls = new HashMap<>(8);
    Set<String> get = new HashSet<>();
    Set<String> post = new HashSet<>();
    Set<String> put = new HashSet<>();
    Set<String> patch = new HashSet<>();
    Set<String> delete = new HashSet<>();
    Set<String> all = new HashSet<>();
    for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
      HandlerMethod handlerMethod = infoEntry.getValue();
      AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
      if (null != anonymousAccess) {
        List<RequestMethod> requestMethods =
            new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods());
        RequestMethodEnum request =
            RequestMethodEnum.find(
                requestMethods.size() == 0
                    ? RequestMethodEnum.ALL.getType()
                    : requestMethods.get(0).name());
        switch (Objects.requireNonNull(request)) {
          case GET:
            get.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
            break;
          case POST:
            post.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
            break;
          case PUT:
            put.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
            break;
          case PATCH:
            patch.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
            break;
          case DELETE:
            delete.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
            break;
          default:
            all.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
            break;
        }
      }
    }
    anonymousUrls.put(RequestMethodEnum.GET.getType(), get);
    anonymousUrls.put(RequestMethodEnum.POST.getType(), post);
    anonymousUrls.put(RequestMethodEnum.PUT.getType(), put);
    anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch);
    anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete);
    anonymousUrls.put(RequestMethodEnum.ALL.getType(), all);

    return anonymousUrls;
  }
}
